Node.js 면접 준비
작성일 : 2024.12.06
1. Event Loop와 비동기 처리
- Event Loop: JavaScript가 싱글스레드임에도 비동기 작업을 처리할 수 있게 하는 메커니즘
- 실행 과정:
- 콜스택에서 동기 코드 실행
- 비동기 작업은 Web API로 이동
- 완료된 작업은 콜백큐로 이동
- Event Loop가 콜스택이 비었을 때 콜백큐의 작업을 콜스택으로 이동
예시 코드:
console.log('1');
setTimeout(() => console.log('2'), 0);
console.log('3');
// 출력: 1, 3, 2
모범답안
Event Loop는 Node.js의 비동기 처리를 가능하게 하는 핵심 메커니즘입니다. 싱글스레드인 JavaScript가 비동기 작업을 처리할 수 있게 해주죠.
동작 방식을 설명하면:
1) 동기 코드는 콜스택에서 즉시 실행됩니다
2) setTimeout 같은 비동기 작업은 Web API로 넘어갑니다
3) 완료된 작업은 콜백큐로 이동합니다
4) Event Loop가 콜스택이 비었을 때 큐의 작업을 스택으로 가져옵니다
실제 프로젝트에서는 이를 활용해 대용량 파일 처리나 네트워크 요청을 효율적으로 처리할 수 있었습니다.
2. Node.js 특징
싱글스레드:
- 메인 스레드 하나로 동작
- 멀티스레드 대비 컨텍스트 스위칭 비용 감소
- CPU 집약적 작업에는 워커스레드 활용 필요
논블로킹 I/O:
- 파일 읽기/쓰기, 네트워크 요청 등을 비동기로 처리
- 작업 완료를 기다리지 않고 다음 코드 실행
fs.readFile('file.txt', (err, data) => {
if (err) throw err;
console.log(data);
});
console.log('다음 작업 실행');
모범답안
Node.js는 이벤트 기반 비동기 I/O 모델을 사용합 니다.
1) 메인 스레드는 요청을 받아 작업을 이벤트 루프로 전달합니다
2) I/O 작업은 워커 스레드 풀로 위임됩니다
3) 비동기로 처리되어 블로킹 없이 다음 요청을 받을 수 있습니다
4) 완료된 작업은 콜백으로 처리됩니다
실제로 libuv 라이브러리가 이런 비동기 처리를 담당하며, CPU 작업이 많은 경우는 클러스터 모듈로 멀티 프로세스를 활용합니다.
3. Express.js와 실제 개발
미들웨어:
app.use((req, res, next) => {
console.log('로깅 미들웨어');
next();
});
app.get('/', (req, res) => {
res.send('Hello');
});
에러 핸들링:
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('서버 에러 발생');
});
메모리 누수 관리:
- 글로벌 변수 사용 최소화
- 이벤트 리스너 제거
- 스트림 종료 처리
const stream = fs.createReadStream('big.file');
stream.on('end', () => stream.destroy());
모범답안
미들웨어는 요청-응답 사이클에서 순차적으로 실행되는 함수들입니다.
특징:
1) req, res, next 파라미터를 가집니다
2) next()로 다음 미들웨어로 제어를 넘깁니다
3) 순서가 중요하며 에러 처리는 마지막에 위치합니다
주로 인증, 로깅, 에러 처리 등에 활용했으며, 커스텀 미들웨어로 반복되는 로직을 모듈화할 수 있었습니다.
4. 성능 최적화
클러스터링:
const cluster = require('cluster');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
} else {
// 워커 프로세스 코드
}
성능 모니터링:
- New Relic, PM2 등 도구 활용
- 메모리 사용량, CPU 사용률 모니터링
- 응답시간, 처리량 측정
모범답안
Node.js에서 메모리 누수는 주로 다음 원인으로 발생합니다:
1) 글로벌/정적 변수의 무분별한 사용
2) 이벤트 리스너 미제거
3) 클로저의 부적절한 사용
4) 스트림 미종료
해결책:
- heap-snapshot으로 메모리 분석
- 주기적인 가비지 컬렉션
- 리스너 cleanup 코드 작성
- 스트림 작업 후 명시적 종료
실제로 프로젝트에서 메모리 릭을 debugger로 추적하고 해결한 경험이 있습니다.
5. 보안 대책
SQL 인젝션 방지:
// 잘못된 예
db.query(`SELECT * FROM users WHERE id = ${userId}`);
// 올바른 예
db.query('SELECT * FROM users WHERE id = ?', [userId]);
XSS 대책:
- 입력값 이스케이프 처리
- helmet 미들웨어 사용
const helmet = require('helmet');
app.use(helmet());
CSRF 보호:
const csrf = require('csurf');
app.use(csrf({ cookie: true }));
6. REST API 설계 원칙
- 자원을 URI로 표현 (명사 사용)
- 행위는 HTTP 메서드로 표현
- 응답은 적절한 상태코드 사용
- 버저닝 관리
- HATEOAS 원칙 준수
버저닝
- API의 버전을 관리하는 방식
- API 변경시 하위 호환성 유지
- 주요 방법:
- URI:
/api/v1/users - 헤더:
Accept: application/vnd.company.myapp-v1+json - 파라미터:
/users?version=1
- URI:
- URL 베이스 버전:
/api/v1/*형태로 모든 API에 기본 적용- 명시적이지만 URL이 지저분해짐
- 헤더 기반 버전:
- 요청 헤더에 버전 정보 포함
- URL은 깔끔하나 관리가 복잡
HATEOAS (Hypermedia As The Engine Of Application State)
- REST API에서 리소스간 관계를 하이퍼링크로 표현
- 클라이언트가 API 구조를 동적으로 파악 가능
{
"id": 123,
"name": "John",
"_links": {
"self": "/users/123",
"friends": "/users/123/friends",
"profile": "/users/123/profile"
}
}